﻿# nullable enable

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using Microsoft.EntityFrameworkCore.ChangeTracking;

using Microsoft.EntityFrameworkCore;

using Microsoft.IdentityModel.Tokens;
using CommunityToolkit.Diagnostics;

namespace JackProjectTemplate.EntityFrameworkCore.Extensions
{
    public static class DbContextExtensions
    {
        public static DbConnection JackGetDbConnection<TContext>(this TContext context)
            where TContext : DbContext
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Database.GetDbConnection();
        }

        public static DbConnection JackGetDbConnection(this DbContext context)
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Database.GetDbConnection();
        }

        public static TContext JackDelete<TContext, TModel>(this TContext context, TModel? model)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            _ = context.Set<TModel>().Remove(model);
            return context;
        }

        public static DbContext JackDelete<TModel>(this DbContext context, TModel? model)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            _ = context.Set<TModel>().Remove(model);
            return context;
        }

        public static TContext JackDelete<TContext, TModel, TKey>(this TContext context, string keyName, params TKey[] ids)
            where TContext : DbContext
            where TModel : class, new()
        {
            ArgumentNullException.ThrowIfNull(context);

            if (ids.IsNullOrEmpty()) return context;

            TModel[] models = new TModel[ids.Length];
            PropertyInfo? property = typeof(TModel).GetProperty(keyName);
            ThrowHelper.ThrowInvalidOperationException($"当前类型没有[{keyName}]属性");

            for (int i = 0; i < ids.Length; i++)
            {
                TKey key = ids[i];
                TModel model = new();
                property.SetValue(model, key);
                models[i] = model;
            }

            context.Set<TModel>().RemoveRange(models);

            return context;
        }

        public static DbContext JackDelete<TModel, TKey>(this DbContext context, string keyName, params TKey[] ids)
            where TModel : class, new()
        {
            ArgumentNullException.ThrowIfNull(context);

            if (ids.IsNullOrEmpty()) return context;

            TModel[] models = new TModel[ids.Length];
            PropertyInfo? property = typeof(TModel).GetProperty(keyName);
            ThrowHelper.ThrowInvalidOperationException($"当前类型没有[{keyName}]属性");

            for (int i = 0; i < ids.Length; i++)
            {
                TKey key = ids[i];
                TModel model = new();
                property.SetValue(model, key);
                models[i] = model;
            }

            context.Set<TModel>().RemoveRange(models);

            return context;
        }

        public static TContext JackDeleteByDefaultKey<TContext, TModel, TKey>(this TContext context, params TKey[] ids)
            where TContext : DbContext
            where TModel : class, new()
        {
            return JackDelete<TContext, TModel, TKey>(context: context, keyName: "Id", ids: ids);
        }

        public static DbContext JackDeleteByDefaultKey<TModel, TKey>(this DbContext context, params TKey[] ids)
            where TModel : class, new()
        {
            return JackDelete<TModel, TKey>(context: context, keyName: "Id", ids: ids);
        }

        public static TContext JackDelete<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> predicate)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            var dbSet = context.Set<TModel>();
            dbSet.RemoveRange(dbSet.Where(predicate));

            return context;
        }

        public static DbContext JackDelete<TModel>(this DbContext context, Expression<Func<TModel, bool>> predicate)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            var dbSet = context.Set<TModel>();
            dbSet.RemoveRange(dbSet.Where(predicate));

            return context;
        }

        public static TContext JackDeleteMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            context.Set<TModel>().RemoveRange(models.OfType<TModel>());
            return context;
        }

        public static DbContext JackDeleteMany<TModel>(this DbContext context, IEnumerable<TModel?> models)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            context.Set<TModel>().RemoveRange(models.OfType<TModel>());
            return context;
        }

        public static Task<TModel?> JackGet<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Set<TModel>().AsNoTracking().FirstOrDefaultAsync(expression, cancellationToken);
        }

        public static Task<TModel?> JackGet<TModel>(this DbContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Set<TModel>().AsNoTracking().FirstOrDefaultAsync(expression, cancellationToken);
        }

        public static Task<List<TModel>> JackGetList<TContext, TModel>(this TContext context, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Set<TModel>().AsNoTracking().ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetList<TModel>(this DbContext context, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Set<TModel>().AsNoTracking().ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetList<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Set<TModel>().AsNoTracking().Where(expression).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetList<TModel>(this DbContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return context.Set<TModel>().AsNoTracking().Where(expression).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetPageList<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> expression, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return DynamicQueryableExtensions.OrderBy(context.Set<TModel>().AsNoTracking().Where(expression), sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetPageList<TModel>(this DbContext context, Expression<Func<TModel, bool>> expression, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return DynamicQueryableExtensions.OrderBy(context.Set<TModel>().AsNoTracking().Where(expression), sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetPageList<TContext, TModel>(this TContext context, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return DynamicQueryableExtensions.OrderBy(context.Set<TModel>().AsNoTracking(), sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> JackGetPageList<TModel>(this DbContext context, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            return DynamicQueryableExtensions.OrderBy(context.Set<TModel>().AsNoTracking(), sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static async Task<TContext> JackInsert<TContext, TModel>(this TContext context, TModel? model, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;
            _ = await context.Set<TModel>().AddAsync(model, cancellationToken);

            return context;
        }

        public static async Task<DbContext> JackInsert<TModel>(this DbContext context, TModel? model, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            _ = await context.Set<TModel>().AddAsync(model, cancellationToken);
            return context;
        }

        public static async Task<TContext> JackInsertMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            await context.Set<TModel>().AddRangeAsync(models.OfType<TModel>(), cancellationToken);
            return context;
        }

        public static async Task<DbContext> JackInsertMany<TModel>(this DbContext context, IEnumerable<TModel?> models, CancellationToken cancellationToken = default)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            await context.Set<TModel>().AddRangeAsync(models.OfType<TModel>(), cancellationToken);
            return context;
        }

        public static TContext JackUpdateEx<TContext, TModel>(this TContext context, TModel? model)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            JackAttachIfNot(context, model);
            _ = context.Set<TModel>().Update(model);
            return context;
        }

        public static TContext JackUpdateExMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                JackAttachIfNot(context, model);
                _ = context.Set<TModel>().Update(model);
            }

            return context;
        }

        public static DbContext JackUpdateEx<TModel>(this DbContext context, TModel? model)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            JackAttachIfNot(context, model);
            _ = context.Set<TModel>().Update(model);
            return context;
        }

        public static DbContext JackUpdateExMany<TModel>(this DbContext context, IEnumerable<TModel?> models)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                JackAttachIfNot(context, model);
                _ = context.Set<TModel>().Update(model);
            }

            return context;
        }

        public static TContext JackUpdatePartial<TContext, TModel>(this TContext context, TModel model, params Expression<Func<TModel, object?>>[] properties)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null || properties == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (Expression<Func<TModel, object?>> property in properties)
            {
                entity.Property(property).IsModified = true;
            }

            return context;
        }

        public static TContext JackUpdatePartialMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, params Expression<Func<TModel, object?>>[] properties)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;
            if (properties == null || properties.Length == 0) return context;

            foreach (var model in models.OfType<TModel>())
            {
                var entity = context.Entry(model);
                foreach (Expression<Func<TModel, object?>> property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static DbContext JackUpdatePartial<TModel>(this DbContext context, TModel model, params Expression<Func<TModel, object?>>[] properties)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null || properties == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (Expression<Func<TModel, object?>> property in properties)
            {
                entity.Property(property).IsModified = true;
            }

            return context;
        }

        public static DbContext JackUpdatePartialMany<TModel>(this DbContext context, IEnumerable<TModel> models, params Expression<Func<TModel, object?>>[] properties)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty() || properties == null || properties.Length == 0) return context;

            foreach (var model in models)
            {
                var entity = context.Entry(model);
                foreach (Expression<Func<TModel, object?>> property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static TContext JackUpdatePartial<TContext, TModel>(this TContext context, TModel? model, params string[] properties)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null || properties == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (string property in properties)
            {
                entity.Property(property).IsModified = true;
            }

            return context;
        }

        public static TContext JackUpdatePartialMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, params string[] properties)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty() || properties == null || properties.Length == 0) return context;

            foreach (var model in models.OfType<TModel>())
            {
                var entity = context.Entry(model);
                foreach (string property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static DbContext JackUpdatePartial<TModel>(this DbContext context, TModel? model, params string[] properties)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null || properties == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (string property in properties)
            {
                entity.Property(property).IsModified = true;
            }
            return context;
        }

        public static DbContext JackUpdatePartialMany<TModel>(this DbContext context, IEnumerable<TModel?> models, params string[] properties)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty() || properties == null || properties.Length == 0) return context;

            foreach (var model in models.OfType<TModel>())
            {
                var entity = context.Entry(model);
                foreach (string property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static TContext JackUpdatePartialByExclude<TContext, TModel>(this TContext context, TModel? model, params string[] excludeProperties)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            var entry = context.Entry(model);
            foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
            {
                entry.Property(property.Name).IsModified = true;
            }
            return context;
        }

        public static TContext JackUpdatePartialManyByExclude<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, params string[] excludeProperties)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                var entry = context.Entry(model);
                foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
                {
                    entry.Property(property.Name).IsModified = true;
                }
            }

            return context;
        }

        public static DbContext JackUpdatePartialByExclude<TModel>(this DbContext context, TModel? model, params string[] excludeProperties)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (model == null) return context;

            var entry = context.Entry(model);
            foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
            {
                entry.Property(property.Name).IsModified = true;
            }
            return context;
        }

        public static DbContext JackUpdatePartialManyByExclude<TModel>(this DbContext context, IEnumerable<TModel?> models, params string[] excludeProperties)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                var entry = context.Entry(model);
                foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
                {
                    entry.Property(property.Name).IsModified = true;
                }
            }

            return context;
        }

        public static TContext JackUpdateMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models)
            where TContext : DbContext
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models.OfType<TModel>())
            {
                JackAttachIfNot(context, model);
            }
            context.Set<TModel>().UpdateRange(models.OfType<TModel>());

            return context;
        }

        public static DbContext JackUpdateMany<TModel>(this DbContext context, IEnumerable<TModel?> models)
            where TModel : class
        {
            ArgumentNullException.ThrowIfNull(context);

            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models.OfType<TModel>())
            {
                JackAttachIfNot(context, model);
            }
            context.Set<TModel>().UpdateRange(models.OfType<TModel>());

            return context;
        }

        private static void JackAttachIfNot<TContext, TModel>(TContext context, TModel? model)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null) return;

            EntityEntry? entry = context.ChangeTracker.Entries().FirstOrDefault((EntityEntry ent) => ent.Entity == model);
            if (entry == null)
            {
                context.Attach(model);
            }
        }
    }
}
